﻿namespace JavaScript8000.Scaner
{
    using System.Collections.Generic;
    using System.Text;

    using JavaScript8000.Common;

    /// <summary>
    /// Represent JavaScript scaner.
    /// </summary>
    public class Scaner
    {
        #region Constants and Fields

        /// <summary>
        /// Position of current char.
        /// </summary>
        private int currentChar;

        /// <summary>
        /// Current row.
        /// </summary>
        private int currentRow;

        /// <summary>
        /// Current column.
        /// </summary>
        private int currentColumn;

        /// <summary>
        /// Source code of program.
        /// </summary>
        private string sourceCode;

        /// <summary>
        /// Current token.
        /// </summary>
        private StringBuilder token;

        /// <summary>
        /// Error list.
        /// </summary>
        private List<ScanError> errors;

        /// <summary>
        /// The Tokens.
        /// </summary>
        private List<Token> tokens;

        /// <summary>
        /// User tokens.
        /// </summary>
        private UserTokens userTokens;

        /// <summary>
        /// "{" "}" Counter.
        /// </summary>
        private int figScobe;

        /// <summary>
        /// "[" "]" Counter.
        /// </summary>
        private int sqScobe;

        /// <summary>
        /// "(" ")" Counter.
        /// </summary>
        private int scobe;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Initializes a new instance of the <see cref="Scaner"/> class.
        /// </summary>
        public Scaner()
        {
            this.token = new StringBuilder();
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Scan source code.
        /// </summary>
        /// <param name="sourceCode">
        /// The source code.
        /// </param>
        /// <returns>
        /// Results of scan (token list, error list and userToken table).
        /// </returns>
        public ScanResults Scan(string sourceCode)
        {
            this.sourceCode = sourceCode;
            this.currentChar = 0;
            this.currentColumn = 0;
            this.currentRow = 0;
            this.figScobe = 0;
            this.sqScobe = 0;
            this.scobe = 0;
            this.errors = new List<ScanError>();
            this.tokens = new List<Token>();
            this.userTokens = new UserTokens();

            int l = this.sourceCode.Length;

            while (this.currentChar < l)
            {
                var tok = this.ScanNext();
                if (tok != null)
                {
                    this.tokens.Add(tok);
                }
            }

            while (this.sqScobe > 0)
            {
                this.errors.Add(
                    new ScanError(
                        this.currentRow,
                        this.currentColumn,
                        this.currentChar,
                        "Нехватает закрывающей скобки ]"));
                this.sqScobe--;
            }

            while (this.scobe > 0)
            {
                this.errors.Add(
                    new ScanError(
                        this.currentRow,
                        this.currentColumn,
                        this.currentChar,
                        "Нехватает закрывающей скобки )"));
                this.scobe--;
            }

            while (this.figScobe > 0)
            {
                this.errors.Add(
                    new ScanError(
                        this.currentRow,
                        this.currentColumn,
                        this.currentChar,
                        "Нехватает закрывающей скобки }"));
                this.figScobe--;
            }

            // Add last token.
            this.tokens.Add(new Token(SystemTokens.EofCode));

            return new ScanResults(this.tokens, this.errors, this.userTokens);
        }

        #endregion

        #region Methods

        /// <summary>
        /// Gets next char.
        /// </summary>
        /// <returns>
        /// Return <c>false</c> if can't get next char.
        /// </returns>
        private bool GetChar()
        {
            if (this.sourceCode[this.currentChar] == '\n')
            {
                this.currentRow++;
                this.currentColumn = 0;
            }

            this.currentChar++;
            if (this.currentChar >= this.sourceCode.Length)
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Gets the non blank char.
        /// </summary>
        /// <returns>
        /// Return <c>false</c> if can't get next char.
        /// </returns>
        private bool GetNonBlankChar()
        {
            while (this.sourceCode[this.currentChar] == ' ' || this.sourceCode[this.currentChar] == '\t'
                   || this.sourceCode[this.currentChar] == '\r' || this.sourceCode[this.currentChar] == '\n')
            {
                if (this.sourceCode[this.currentChar] == '\n')
                {
                    this.currentRow++;
                    this.currentColumn = 0;
                }

                this.currentChar++;
                if (this.currentChar >= this.sourceCode.Length)
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Scans the next token.
        /// </summary>
        /// <returns>Next token or null if can't get it.</returns>
        private Token ScanNext()
        {
            this.token.Clear();

            if (!this.GetNonBlankChar())
            {
                return null;
            }

            // Check ID.
            if ((this.sourceCode[this.currentChar] >= 'a' && this.sourceCode[this.currentChar] <= 'z')
                || (this.sourceCode[this.currentChar] >= 'A' && this.sourceCode[this.currentChar] <= 'Z')
                || this.sourceCode[this.currentChar] == '_' || this.sourceCode[this.currentChar] == '$')
            {
                this.token.Append(this.sourceCode[this.currentChar]);
                while (this.GetChar()
                    && ((this.sourceCode[this.currentChar] >= '0' && this.sourceCode[this.currentChar] <= '9')
                    || (this.sourceCode[this.currentChar] >= 'a' && this.sourceCode[this.currentChar] <= 'z')
                    || (this.sourceCode[this.currentChar] >= 'A' && this.sourceCode[this.currentChar] <= 'Z')
                    || this.sourceCode[this.currentChar] == '_' || this.sourceCode[this.currentChar] == '$'))
                {
                    this.token.Append(this.sourceCode[this.currentChar]);
                }

                string name = this.token.ToString();
                int code = SystemTokens.GetCode(name);
                if (code == 0)
                {
                    return new Token(SystemTokens.IdCode, this.userTokens.GetUserCode(name));
                }
                switch (name)
                {
                    case "true":
                    case "false":
                        return new Token(SystemTokens.BoolLiteralCode, this.userTokens.GetUserCode(name));
                    case "null":
                        return new Token(SystemTokens.NullLiteral, this.userTokens.GetUserCode(name));
                    default:
                        return new Token(code);
                }
            }

            // Numeric Literals.
            if (this.sourceCode[this.currentChar] >= '0' && this.sourceCode[this.currentChar] <= '9')
            {
                while (this.sourceCode[this.currentChar] >= '0' && this.sourceCode[this.currentChar] <= '9')
                {
                    this.token.Append(this.sourceCode[this.currentChar]);
                    if (!this.GetChar())
                    {
                        return new Token(SystemTokens.NumericLiteralCode, this.userTokens.GetUserCode(this.token.ToString()));
                    }
                }

                if (this.sourceCode[this.currentChar] == '.')
                {
                    this.token.Append(this.sourceCode[this.currentChar]);

                    if (this.GetChar())
                    {
                        if (this.sourceCode[this.currentChar] < '0' || this.sourceCode[this.currentChar] > '9')
                        {
                            this.errors.Add(new ScanError(this.currentRow, this.currentColumn, this.currentChar, "Ошибка в запииси числового литерала"));
                            return this.ScanNext();
                        }

                        while (this.sourceCode[this.currentChar] >= '0' && this.sourceCode[this.currentChar] <= '9')
                        {
                            this.token.Append(this.sourceCode[this.currentChar]);
                            if (!this.GetChar())
                            {
                                return new Token(SystemTokens.NumericLiteralCode, this.userTokens.GetUserCode(this.token.ToString()));
                            }
                        }

                        if (this.sourceCode[this.currentChar] == 'e' || this.sourceCode[this.currentChar] == 'E')
                        {
                            this.token.Append(this.sourceCode[this.currentChar]);
                            if (!this.GetChar())
                            {
                                this.errors.Add(new ScanError(this.currentRow, this.currentColumn, this.currentChar, "Неожиданый конец кода при распознавании числового литерала"));
                                return null;
                            }

                            if (this.sourceCode[this.currentChar] == '+' || this.sourceCode[this.currentChar] == '-')
                            {
                                this.token.Append(this.sourceCode[this.currentChar]);
                                if (!this.GetChar())
                                {
                                    this.errors.Add(new ScanError(this.currentRow, this.currentColumn, this.currentChar, "Неожиданый конец кода при распознавании числового литерала"));
                                    return null;
                                }
                            }

                            if (this.sourceCode[this.currentChar] >= '0' && this.sourceCode[this.currentChar] <= '9')
                            {
                                while (this.sourceCode[this.currentChar] >= '0' && this.sourceCode[this.currentChar] <= '9')
                                {
                                    this.token.Append(this.sourceCode[this.currentChar]);
                                    if (!this.GetChar())
                                    {
                                        break;
                                    }
                                }

                                return new Token(SystemTokens.NumericLiteralCode, this.userTokens.GetUserCode(this.token.ToString()));
                            }
                            else
                            {
                                this.errors.Add(new ScanError(this.currentRow, this.currentColumn, this.currentChar, "Ошибка в написании числового литерала"));
                                return this.ScanNext();
                            }
                        }

                        return new Token(SystemTokens.NumericLiteralCode, this.userTokens.GetUserCode(this.token.ToString()));
                    }
                    else
                    {
                        this.errors.Add(new ScanError(this.currentRow, this.currentColumn, this.currentChar, "Неожиданый конец кода при распознавании числового литерала"));
                        return null;
                    }
                }
                else if (this.sourceCode[this.currentChar] == 'e' || this.sourceCode[this.currentChar] == 'E')
                {
                    this.token.Append(this.sourceCode[this.currentChar]);
                    if (!this.GetChar())
                    {
                        this.errors.Add(new ScanError(this.currentRow, this.currentColumn, this.currentChar, "Неожиданый конец кода при распознавании числового литерала"));
                        return null;
                    }

                    if (this.sourceCode[this.currentChar] == '+' || this.sourceCode[this.currentChar] == '-')
                    {
                        this.token.Append(this.sourceCode[this.currentChar]);
                        if (!this.GetChar())
                        {
                            this.errors.Add(new ScanError(this.currentRow, this.currentColumn, this.currentChar, "Неожиданый конец кода при распознавании числового литерала"));
                            return null;
                        }
                    }

                    if (this.sourceCode[this.currentChar] >= '0' && this.sourceCode[this.currentChar] <= '9')
                    {
                        while (this.sourceCode[this.currentChar] >= '0' && this.sourceCode[this.currentChar] <= '9')
                        {
                            this.token.Append(this.sourceCode[this.currentChar]);
                            if (!this.GetChar())
                            {
                                break;
                            }
                        }

                        return new Token(SystemTokens.NumericLiteralCode, this.userTokens.GetUserCode(this.token.ToString()));
                    }
                    else
                    {
                        this.errors.Add(new ScanError(this.currentRow, this.currentColumn, this.currentChar, "Ошибка в написании числового литерала"));
                        return this.ScanNext();
                    }
                }

                return new Token(SystemTokens.NumericLiteralCode, this.userTokens.GetUserCode(this.token.ToString()));
            }

            // Check operators.
            switch (this.sourceCode[this.currentChar])
            {
                case '{':
                    this.figScobe++;
                    this.token.Append(this.sourceCode[this.currentChar]);
                    this.GetChar();
                    break;
                case '(':
                    this.scobe++;
                    this.token.Append(this.sourceCode[this.currentChar]);
                    this.GetChar();
                    break;
                case '[':
                    this.sqScobe++;
                    this.token.Append(this.sourceCode[this.currentChar]);
                    this.GetChar();
                    break;
                case '}':
                    this.figScobe--;

                    if (this.figScobe < 0)
                    {
                        this.figScobe = 0;
                        this.errors.Add(
                            new ScanError(
                                this.currentRow,
                                this.currentColumn,
                                this.currentChar,
                                "Нехватает открывающей скобки {"));
                    }

                    this.token.Append(this.sourceCode[this.currentChar]);
                    this.GetChar();
                    break;
                case ')':
                    this.scobe--;

                    if (this.scobe < 0)
                    {
                        this.scobe = 0;
                        this.errors.Add(
                            new ScanError(
                                this.currentRow,
                                this.currentColumn,
                                this.currentChar,
                                "Нехватает открывающей скобки ("));
                    }

                    this.token.Append(this.sourceCode[this.currentChar]);
                    this.GetChar();
                    break;
                case ']':
                    this.sqScobe--;

                    if (this.sqScobe < 0)
                    {
                        this.sqScobe = 0;
                        this.errors.Add(
                            new ScanError(
                                this.currentRow,
                                this.currentColumn,
                                this.currentChar,
                                "Нехватает открывающей скобки ["));
                    }

                    this.token.Append(this.sourceCode[this.currentChar]);
                    this.GetChar();
                    break;
                case ';':
                    this.token.Append(this.sourceCode[this.currentChar]);
                    this.GetChar();
                    break;
                case '.':
                    this.token.Append(this.sourceCode[this.currentChar]);

                    if (this.GetChar())
                    {
                        if (this.sourceCode[this.currentChar] < '0' || this.sourceCode[this.currentChar] > '9')
                        {
                            break;
                        }

                        while (this.sourceCode[this.currentChar] >= '0' && this.sourceCode[this.currentChar] <= '9')
                        {
                            this.token.Append(this.sourceCode[this.currentChar]);
                            if (!this.GetChar())
                            {
                                return new Token(SystemTokens.NumericLiteralCode, this.userTokens.GetUserCode(this.token.ToString()));
                            }
                        }

                        if (this.sourceCode[this.currentChar] == 'e' || this.sourceCode[this.currentChar] == 'E')
                        {
                            this.token.Append(this.sourceCode[this.currentChar]);
                            if (!this.GetChar())
                            {
                                this.errors.Add(new ScanError(this.currentRow, this.currentColumn, this.currentChar, "Неожиданый конец кода при распознавании числового литерала"));
                                return null;
                            }

                            if (this.sourceCode[this.currentChar] == '+' || this.sourceCode[this.currentChar] == '-')
                            {
                                this.token.Append(this.sourceCode[this.currentChar]);
                                if (!this.GetChar())
                                {
                                    this.errors.Add(new ScanError(this.currentRow, this.currentColumn, this.currentChar, "Неожиданый конец кода при распознавании числового литерала"));
                                    return null;
                                }
                            }

                            if (this.sourceCode[this.currentChar] >= '0' && this.sourceCode[this.currentChar] <= '9')
                            {
                                while (this.sourceCode[this.currentChar] >= '0' && this.sourceCode[this.currentChar] <= '9')
                                {
                                    this.token.Append(this.sourceCode[this.currentChar]);
                                    if (!this.GetChar())
                                    {
                                        break;
                                    }
                                }

                                return new Token(SystemTokens.NumericLiteralCode, this.userTokens.GetUserCode(this.token.ToString()));
                            }
                            else
                            {
                                this.errors.Add(new ScanError(this.currentRow, this.currentColumn, this.currentChar, "Ошибка в написании числового литерала"));
                                return this.ScanNext();
                            }
                        }

                        return new Token(SystemTokens.NumericLiteralCode, this.userTokens.GetUserCode(this.token.ToString()));
                    }

                    break;
                case ',':
                    this.token.Append(this.sourceCode[this.currentChar]);
                    this.GetChar();
                    break;
                case '?':
                    this.token.Append(this.sourceCode[this.currentChar]);
                    this.GetChar();
                    break;
                case ':':
                    this.token.Append(this.sourceCode[this.currentChar]);
                    this.GetChar();
                    break;
                case '~':
                    this.token.Append(this.sourceCode[this.currentChar]);
                    this.GetChar();
                    break;

                case '+':
                    this.token.Append(this.sourceCode[this.currentChar]);
                    if (this.GetChar())
                    {
                        if (this.sourceCode[this.currentChar] == '+' || this.sourceCode[this.currentChar] == '=')
                        {
                            this.token.Append(this.sourceCode[this.currentChar]);
                            this.GetChar();
                        }
                    }

                    break;
                case '-':
                    this.token.Append(this.sourceCode[this.currentChar]);
                    if (this.GetChar())
                    {
                        if (this.sourceCode[this.currentChar] == '-' || this.sourceCode[this.currentChar] == '=')
                        {
                            this.token.Append(this.sourceCode[this.currentChar]);
                            this.GetChar();
                        }
                    }

                    break;
                case '=':
                    this.token.Append(this.sourceCode[this.currentChar]);
                    if (this.GetChar() && this.sourceCode[this.currentChar] == '=')
                    {
                        this.token.Append(this.sourceCode[this.currentChar]);
                        if (this.GetChar() && this.sourceCode[this.currentChar] == '=')
                        {
                            this.token.Append(this.sourceCode[this.currentChar]);
                            this.GetChar();
                        }
                    }

                    break;
                case '!':
                    this.token.Append(this.sourceCode[this.currentChar]);
                    if (this.GetChar() && this.sourceCode[this.currentChar] == '=')
                    {
                        this.token.Append(this.sourceCode[this.currentChar]);
                        if (this.GetChar() && this.sourceCode[this.currentChar] == '=')
                        {
                            this.token.Append(this.sourceCode[this.currentChar]);
                            this.GetChar();
                        }
                    }

                    break;
                case '*':
                    this.token.Append(this.sourceCode[this.currentChar]);
                    if (this.GetChar() && this.sourceCode[this.currentChar] == '=')
                    {
                        this.token.Append(this.sourceCode[this.currentChar]);
                        this.GetChar();
                    }

                    break;
                case '%':
                    this.token.Append(this.sourceCode[this.currentChar]);
                    if (this.GetChar() && this.sourceCode[this.currentChar] == '=')
                    {
                        this.token.Append(this.sourceCode[this.currentChar]);
                        this.GetChar();
                    }

                    break;
                case '^':
                    this.token.Append(this.sourceCode[this.currentChar]);
                    if (this.GetChar() && this.sourceCode[this.currentChar] == '=')
                    {
                        this.token.Append(this.sourceCode[this.currentChar]);
                        this.GetChar();
                    }

                    break;
                case '>':
                    this.token.Append(this.sourceCode[this.currentChar]);
                    if (this.GetChar())
                    {
                        if (this.sourceCode[this.currentChar] == '=')
                        {
                            this.token.Append(this.sourceCode[this.currentChar]);
                            this.GetChar();
                        }
                        else if (this.sourceCode[this.currentChar] == '>')
                        {
                            this.token.Append(this.sourceCode[this.currentChar]);
                            if (this.GetChar())
                            {
                                if (this.sourceCode[this.currentChar] == '=')
                                {
                                    this.token.Append(this.sourceCode[this.currentChar]);
                                    this.GetChar();
                                }
                                else if (this.sourceCode[this.currentChar] == '>')
                                {
                                    this.token.Append(this.sourceCode[this.currentChar]);
                                    if (this.GetChar() && this.sourceCode[this.currentChar] == '=')
                                    {
                                        this.token.Append(this.sourceCode[this.currentChar]);
                                        this.GetChar();
                                    }
                                }
                            }
                        }
                    }

                    break;
                case '<':
                    this.token.Append(this.sourceCode[this.currentChar]);
                    if (this.GetChar())
                    {
                        if (this.sourceCode[this.currentChar] == '=')
                        {
                            this.token.Append(this.sourceCode[this.currentChar]);
                            this.GetChar();
                        }
                        else if (this.sourceCode[this.currentChar] == '<')
                        {
                            this.token.Append(this.sourceCode[this.currentChar]);
                            if (this.GetChar() && this.sourceCode[this.currentChar] == '=')
                            {
                                this.token.Append(this.sourceCode[this.currentChar]);
                                this.GetChar();
                            }
                        }
                    }

                    break;
                case '&':
                    this.token.Append(this.sourceCode[this.currentChar]);
                    if (this.GetChar()
                        && (this.sourceCode[this.currentChar] == '&' || this.sourceCode[this.currentChar] == '='))
                    {
                        this.token.Append(this.sourceCode[this.currentChar]);
                        this.GetChar();
                    }

                    break;
                case '|':
                    this.token.Append(this.sourceCode[this.currentChar]);
                    if (this.GetChar()
                        && (this.sourceCode[this.currentChar] == '|' || this.sourceCode[this.currentChar] == '='))
                    {
                        this.token.Append(this.sourceCode[this.currentChar]);
                        this.GetChar();
                    }

                    break;
                case '/':
                    this.token.Append(this.sourceCode[this.currentChar]);
                    if (this.GetChar())
                    {
                        switch (this.sourceCode[this.currentChar])
                        {
                            case '/':
                                while (this.sourceCode[this.currentChar] != '\n')
                                {
                                    if (!this.GetChar())
                                    {
                                        return null;
                                    }
                                }

                                return this.ScanNext();
                            case '*':
                                bool wasPoint = false;
                                while (!wasPoint || this.sourceCode[this.currentChar] != '/')
                                {
                                    if (!this.GetChar())
                                    {
                                        this.errors.Add(
                                            new ScanError(
                                                this.currentRow,
                                                this.currentColumn,
                                                this.currentChar,
                                                "Коментарий /* не закрыт"));
                                        return null;
                                    }

                                    wasPoint = this.sourceCode[this.currentChar] != '*';
                                }

                                if (!this.GetChar())
                                {
                                    return null;
                                }

                                return this.ScanNext();
                            case '=':
                                this.token.Append(this.sourceCode[this.currentChar]);
                                this.GetChar();
                                break;
                        }
                    }

                    break;
                case '\"':
                    this.token.Append(this.sourceCode[this.currentChar]);

                    bool wasSlash = false;
                    while (this.GetChar())
                    {
                        this.token.Append(this.sourceCode[this.currentChar]);

                        if (!wasSlash && this.sourceCode[this.currentChar] == '\"')
                        {
                            //this.token.Remove(this.token.Length - 1, 1);
                            this.GetChar();
                            return new Token(SystemTokens.StringLiteralCode, this.userTokens.GetUserCode(this.token.ToString()));
                        }

                        wasSlash = this.sourceCode[this.currentChar] == '\\';
                    }

                    this.errors.Add(
                        new ScanError(this.currentRow, this.currentColumn, this.currentChar, "Не закрыта двойная кавычка."));
                    return null;
                case '\'':
                    this.token.Append(this.sourceCode[this.currentChar]);

                    bool wasSlash2 = false;
                    while (this.GetChar())
                    {
                        this.token.Append(this.sourceCode[this.currentChar]);

                        if (!wasSlash2 && this.sourceCode[this.currentChar] == '\'')
                        {
                            //this.token.Remove(this.token.Length - 1, 1);
                            this.GetChar();
                            return new Token(SystemTokens.StringLiteralCode, this.userTokens.GetUserCode(this.token.ToString()));
                        }

                        wasSlash2 = this.sourceCode[this.currentChar] == '\\';
                    }

                    this.errors.Add(
                        new ScanError(this.currentRow, this.currentColumn, this.currentChar, "Не закрыта одинарная кавычка."));
                    return null;
                default:
                    this.errors.Add(
                        new ScanError(this.currentRow, this.currentColumn, this.currentChar, "Неожиданый символ."));
                    return null;
            }

            return new Token(SystemTokens.GetCode(this.token.ToString()));
        }

        #endregion
    }
}